#pragma once

#include <gl\glew.h>
#include "common_impl.h"
#include "device_impl.h"

namespace blu
{
	namespace impl
	{
		enum class BufferUsage
		{
			generic,

			vertex_indices,

			indirect_arguments,

			program_static_memory,
		};

		template <BufferUsage Usage>
		struct OpenGLMemoryUsageMap
		{

		};

		template <>
		struct OpenGLMemoryUsageMap<BufferUsage::generic>
		{
			static constexpr ::GLenum gl_buffer_type = GL_ARRAY_BUFFER;

			static constexpr ::GLenum gl_binding_type = GL_ARRAY_BUFFER_BINDING;
		};

		template <>
		struct OpenGLMemoryUsageMap<BufferUsage::vertex_indices>
		{
			static constexpr ::GLenum gl_buffer_type = GL_ELEMENT_ARRAY_BUFFER;

			static constexpr ::GLenum gl_binding_type = GL_ELEMENT_ARRAY_BUFFER_BINDING;
		};

		template <>
		struct OpenGLMemoryUsageMap<BufferUsage::program_static_memory>
		{
			static constexpr ::GLenum gl_buffer_type = GL_UNIFORM_BUFFER;

			static constexpr ::GLenum gl_binding_type = GL_UNIFORM_BUFFER_BINDING;
		};

		template <>
		struct OpenGLMemoryUsageMap<BufferUsage::indirect_arguments>
		{
			static constexpr ::GLenum gl_buffer_type = GL_DRAW_INDIRECT_BUFFER;

			static constexpr ::GLenum gl_binding_type = GL_DRAW_INDIRECT_BUFFER_BINDING;
		};

		template <BufferUsage Usage>
		class Buffer;

		template <BufferUsage Usage>
		struct DerivedGLObjectTraits<Buffer<Usage>>
		{
			static ::GLuint construct()
			{
				::GLuint h;
				::glGenBuffers(1, &h);
				return h;
			}

			static void deconstruct(::GLuint h)
			{
				::glDeleteBuffers(1, &h);
			}

			static ::GLuint current()
			{
				::GLint h;
				::glGetIntegerv(OpenGLMemoryUsageMap<Usage>::gl_binding_type, &h);
				return static_cast<::GLuint>(h);
			}

			static void make_current(::GLuint h)
			{
				::glBindBuffer(OpenGLMemoryUsageMap<Usage>::gl_buffer_type, h);
			}
		};

		template <BufferUsage Usage>
		class Buffer
			:public impl::GLObjectBase<Buffer<Usage>>
		{
			typedef impl::GLObjectBase<Buffer<Usage>> MyBase;

			static constexpr ::GLenum MyGLBufferType =
				OpenGLMemoryUsageMap<Usage>::gl_buffer_type;

			static constexpr ::GLenum MyGLBindingType =
				OpenGLMemoryUsageMap<Usage>::gl_binding_type;
		public:
			typedef std::uint32_t size_type;

			Buffer(impl::Device &dvc)
				:MyBase(dvc)
			{

			}

			Buffer(impl::Device &dvc, size_type size)
				:Buffer(dvc, nullptr, size)
			{

			}

			Buffer(impl::Device &dvc, const char *data, size_type size)
				:MyBase(dvc)
			{
				pushAndMakeCurrent();
				::glBufferData(MyGLBufferType, static_cast<::GLsizeiptr>(size), data, GL_STATIC_DRAW);
				popCurrent();
			}

			void write(const char *data, size_type sz, size_type offset = 0)
			{
				pushAndMakeCurrent();
				::glBufferSubData(MyGLBufferType,
					static_cast<::GLintptr>(offset),
					static_cast<::GLsizeiptr>(size),
					static_cast<const void*>(data));
				popCurrent();
			}

			size_type size() const
			{
				pushAndMakeCurrent();
				::GLint sz;
				::glGetBufferParameteriv(MyGLBufferType, GL_BUFFER_SIZE, &sz);
				popCurrent();
			}
		};

		template <BufferUsage Usage>
		class BufferMirror
		{
		public:
			typedef Buffer<Usage> buffer_type;
		private:
			typedef typename buffer_type::size_type MySizeType;

			constexpr static ::GLenum MyGLBufferType =
				OpenGLMemoryUsageMap<Usage>::gl_buffer_type;
		public:
			BufferMirror(
				buffer_type &buffer_,
				MySizeType offset_,
				MySizeType size_)
			{
				buffer_.pushAndMakeCurrent();
				::glMapBufferRange(MyGLBufferType,
					static_cast<::GLintptr>(offset_),
					static_cast<::GLsizeiptr>(size_),
					GL_MAP_READ_BIT | GL_MAP_WRITE_BIT);
				buffer_.popCurrent();
			}

			void release()
			{
				if (_unmapped)
				{
					_unmapped = true;
					buffer_.pushAndMakeCurrent();
					::glUnmapBuffer(MyGLBufferType);
					buffer_.popCurrent();
				}
			}

			~BufferMirror()
			{
				release();
			}

		private:
			buffer_type *_buf;
			bool _unmapped = false;
		};
	}
}